# coding:utf-8
"""
工程商相关接口
"""
import time

import os
import json
from datetime import datetime, date, timedelta

from flask import request, jsonify, current_app
from flask_restful import Api, Resource
from wechatpy.exceptions import WeChatClientException
from sqlalchemy import text

from app.api.supplier_codes import (
    NOT_MATCH_FREE_OFFER, NOT_MATCH_SECOND_OFFER,
    MUST_BE_KEEP, LOW_BID_POINT, BID_LOWER_THAN_SELF,
    ONE_DAY_DISABLE_FEEDBACK, OUT_OF_FREE_REASON_CNT,
    OUT_OF_REASON_CNT, LOCKED_CANNOT_FEEDBACK,
    OFFER_NOT_EQ_EXPECTED, USER_NOT_BRONZE, USER_BRONZE_INVALID,
    IS_USER_LEVEL_6, DISPATCH_ALREADY_BID_BY_COLLEAGUE,
    DISPATCH_CANNOT_FOLLOW,
    DEMAND_LOCKED, APPLY_REAL_NUM_ALREADY, NOT_WIN_DEMAND, GIVE_UP_CANNOT_APPLY_REAL_NUM,
    VOUCHER_PARTIC_CANNOT_FEEDBACK, DEMAND_DONE, CAN_NOT_GET_BEFORE,
    REDEEM_NOT_EXISTED, REDEEM_INACTIVE, REDEEM_USED)
from app.auth.constant import PROTOCOL_VERSION_KEY
from app.axb.models import VirtualNumber, QueryCallNumberRecord
from app.enterprise.constant import EnterpriseLevel
from app.models import get_timestamp
from app.msg.utils import generate_sms_content
from app.op.utils import get_demand_price

from app.wechat.models import WechatTemplate
from common.wechat_util import client
from .. import db
from app.api.constant import TOKEN_TIMEOUT, OpType, MUNICIPAL_CODE, LOCKED_PAID_BID_POINT
from app.api.utils import (
    generate_demand_list_json, generate_captcha,
    RandomDemandCount, calc_bronze_bid_point)

from app.op.models import (
    User, Demand, Partic, Area, WxUser,
    UserTag, UserProtocol, BidCostRecord, UserInfo,
    RenewableDemand, Voucher, RedeemCode)
from app.op.constant import (
    UserType, DemandStatus, ParticStatus, UserStatus, UserActive, DemandTag,
    WxInviteState, AreaType, BusinessDisplay,
    UserCreditRule, FeedbackStatus, BidCostType, UserLevel, MAX_OFFER, FollowType, MONTHLY_REFUSE_MAX,
    ApplyRealNumberState, ContactState, OperatedStatus)
from common.models import Feedback
from app.token import AuthToken
from .codes import USER_NOT_EXIST, SUCCESS, UNAUTHORIZED, PARAMS_ILLEGAL
from .views import AuthResource, Response
from .supplier_schemas import (
    level_prov_sup_in_schema, user_demand_schema,
    demand_bid_schema, pack_schema, supplier_activate_schema,
    user_tag_schema, supplier_join_schema,
    demand_id_in_schema, supplier_invite_users_schema,
    feedback_schema, vouchers_schema, redeem_code_schema)
from .user_schemas import user_edit_schema
from .ip_utils import ip_util
from .utils import generate_demand_info_json
from .supplier_codes import (
    DEMAND_NOT_EXIST, DEMAND_NOT_ONGOING, BID_ALREADY,
    NO_PACK, OUT_OF_PACK, SUP_ALREADY_AUTH, NOT_ENGINEER,
    ALREADY_ENGINEER, PARTIC_ALREADY, ACTIVATE_NO_AVATAR, USER_BLACK,
    NO_PARTIC, FEEDBACK_DUPLICATE, BID_LOWER, BID_LOWER_THAN_START
)
from app.wechat.utils import generate_invite_card
from app.wechat.constant import INVITE_CARD_CACHE_KEY, INVITE_CARD_CACHE_EX
from app.wph_lib import alidayu
from app.msg.models import SMSLog
from app.auth.constant import CaptchaSmsChannel
from common.constant import (
    Captcha_Sms_Channel_Key, TAIWAN_CODE, RedeemStatus, RedeemGiftType,
    GiveVoucherReason)

api = Api()


@api.resource('/Supplier/getCntByProvAndLevel')
class LevelProvSupResource(Resource):
    """
    created by chenhj at 2017/8/14
    获取同等级同省份的工程商数量
    """
    def post(self):
        """
        created by chenhj at 2017/8/14

        """
        data, errors = level_prov_sup_in_schema.load(request.form)
        if data:
            cnt_info = {
                'cnt': User.get_sup_count_by_cond(data['prov'], data['level'])
            }
        else:
            cnt_info = {
                'cnt': 0
            }
        return Response(SUCCESS, cnt_info)


@api.resource('/Supplier/getSupplierCntByProv')
class ProvSupResource(Resource):
    """
    created by chenhj at 2017/8/14
    获取同省所有工程商数量, 全国工程商数量
    """
    def post(self):
        """
        created by chenhj at 2017/8/14

        """
        prov = request.form.get('prov', '')
        if not prov:
            ip_info = ip_util.find(request.remote_addr)
            prov = ip_info[0] if ip_info else '北京市'
        return Response(SUCCESS, {
            'cnt': User.get_sup_count_by_cond(prov),
            'total': User.get_all_sup_count()
        })


@api.resource('/Supplier/Demand/getList4User')
class UserDemandResource(AuthResource):
    """
    created by chenhj at 2017/8/14
    用户已完成/进行中的项目列表
    """
    def post(self):
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        data, errors = user_demand_schema.load(request.form)
        if errors:
            return Response(PARAMS_ILLEGAL.new_format_msg(list(errors.values())[0][0]))
        user = User.get_by_id(data['uid'])
        if not user:
            return Response(USER_NOT_EXIST)
        return Response(SUCCESS, {
            'list': generate_demand_list_json(
                user.get_done_and_ongoing_demands(), user
            ),
        })


@api.resource('/Supplier/Demand/getParticDemandList4CurrUser')
class ParticDemand4CurrentUserResource(AuthResource):
    """
    created by chenhj at 2017/8/14
    我参加的项目列表
    """
    def post(self):
        """
        created by chenhj at 2017/8/14

        """
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        return Response(SUCCESS, {
            'list': generate_demand_list_json(
                self.user.get_partic_demands(), self.user
            ),
        })


@api.resource('/Supplier/Demand/getTodayCnt')
class TodayDemandResource(Resource):
    """
    created by chenhj at 2017/8/14
    查询今日我参与的和所有的项目数目
    ref: https://tower.im/projects/47b71f052c8441eba37547784ba957c3/docs/8241a9ab75af4a16af00ec2843c63a28/
    """
    def post(self):
        token = request.cookies.get('opc_token')
        # 如果是工程商登录, 则参与状态改为该工程商对此项目的参与状态
        user = AuthToken.verify_auth_token(token) if token else None

        me_cnt = 0
        if user:
            if not user.pack:
                area_id = [user.area_id]
            else:
                area_id = [i.id for i in user.attention_areas]
            me_cnt = Demand.get_today_active_demands_count_by_area(area_id)

        return Response(SUCCESS, {
            'me_cnt': me_cnt,
            'total_cnt': RandomDemandCount().demand_count()
        })


@api.resource('/Supplier/Demand/bid')
class DemandBidResource(AuthResource):
    """
    created by chenhj at 2017/8/14
    抢单
    """
    def post(self):
        """
        created by chenhj at 2017/8/14

        """
        # FIXME by chenhj at 2018/5/14 此接口逻辑越来越复杂, 后续有时间需要重构
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)
        # 拉黑用户不允许竞价
        if self.user.black:
            return Response(USER_BLACK)

        data, errors = demand_bid_schema.load(request.form)
        if errors:
            return Response(PARAMS_ILLEGAL.new_format_msg(list(errors.values())[0][0]))
        demand = Demand.get_by_id(data['id'])
        if not demand:
            return Response(DEMAND_NOT_EXIST)

        if demand.is_locked:
            return Response(DEMAND_LOCKED)

        # 签署用户协议
        protocol_version = current_app.redis.get(PROTOCOL_VERSION_KEY) or ''
        if protocol_version and not UserProtocol.get_by_args(
                user_id=self.user.id, version=protocol_version
        ):
            protocol = UserProtocol.create(
                user_id=self.user.id,
                mobile=self.user.mobile,
                nickname=self.user.nickname,
                version=protocol_version
            )
            # FIXME by chenhj at 2017/10/30 产品要求: 当该用户的创建时间在2017-10-18(用户协议版本上线时间)之前的话,
            #  则用户协议表的创建时间同步为该用户的创建时间, 否则为当前时间, 2017-10-18的时间戳为1508256000
            if self.user.created_at <= 1508256000:
                protocol.created_at = self.user.created_at
                protocol.updated_at = self.user.created_at

        dispatch_partic = Partic.query.filter(
            Partic.demand_id == demand.id,
            Partic.supplier_id == self.user.id,
            Partic.demand_status.in_([
                DemandStatus.DISPATCHING.value,
                DemandStatus.MASTER_DISPATCHING.value
            ])
        ).first()

        # 派单用户在圆满完成前均可抢单
        if dispatch_partic:
            if dispatch_partic.status == ParticStatus.SUCCEED.value:
                return Response(BID_ALREADY)

            if demand.status in (
                DemandStatus.DRAFT.value, DemandStatus.UNCHECKED.value,
                DemandStatus.DELAY.value, DemandStatus.DONE.value, DemandStatus.INVALID.value
            ):
                return Response(DEMAND_NOT_ONGOING)

            # 判断该企业下是否已有派单抢单成功用户
            if self.user.highest_level != EnterpriseLevel.LEVEL_6.value:
                # 非地方合作伙伴, 取同企业下中标人
                enterprise_cond = (Partic.enterprise_id == dispatch_partic.enterprise_id)
            else:
                # 地方合作伙伴, 取所有地方合作伙伴企业下中标人
                enterprise_cond = (Partic.enterprise_level == EnterpriseLevel.LEVEL_6.value)
            winner = Partic.query.filter(
                Partic.demand_id == demand.id,
                Partic.demand_status.in_([
                    DemandStatus.DISPATCHING.value,
                    DemandStatus.MASTER_DISPATCHING.value
                ]),
                Partic.status == ParticStatus.SUCCEED.value,
                enterprise_cond
            ).first()
            if winner:
                return Response(
                    DISPATCH_ALREADY_BID_BY_COLLEAGUE,
                    {
                        'winner': {
                            'nickname': winner.owner.nickname,
                            'mobile': '****' + winner.owner.mobile[-4:]
                        }
                    }
                )

            # 入winner_consumer
            current_app.winner_producer.send(
                json.dumps(dict(
                    func='dispatch_assign_winner',
                    args=(),
                    kwargs=dict(
                        partic_id=dispatch_partic.id,
                        demand_id=demand.id,
                    )
                ))
            )
            return Response(SUCCESS, {
                'demand_info': generate_demand_info_json(demand, self.user)
            })
        else:
            if demand.status not in (
                DemandStatus.BRONZE_MEDAL_DISCOUNT.value,
                DemandStatus.ONGOING.value,
                DemandStatus.FREE_ONGOING.value,
                DemandStatus.SECOND_ONGOING.value
            ):
                return Response(DEMAND_NOT_ONGOING)

        offer = data['offer']

        if demand.status == DemandStatus.BRONZE_MEDAL_DISCOUNT.value:
            # 判断用户是否为铜牌
            if not self.user.is_bronze_medal:
                return Response(USER_NOT_BRONZE)
            # 判断用户铜牌是否到期
            if not self.user.bronze_medal_valid:
                return Response(USER_BRONZE_INVALID)

            # 判断offer是否等于期望成交价
            target_offer = calc_bronze_bid_point(demand)
            if target_offer != offer:
                return Response(OFFER_NOT_EQ_EXPECTED)

        # 免费竞价出价必须为0
        if demand.status == DemandStatus.FREE_ONGOING.value:
            if offer != 0:
                return Response(NOT_MATCH_FREE_OFFER)

        # 二次竞价的出价固定为8
        if demand.status == DemandStatus.SECOND_ONGOING.value:
            price = get_demand_price(demand)
            if offer != price:
                return Response(NOT_MATCH_SECOND_OFFER.new_format_msg(price))

        win_partic = Partic.query.filter(
            Partic.demand_id == demand.id,
            Partic.supplier_id == self.user.id,
            Partic.status.in_([ParticStatus.SUCCEED.value, ParticStatus.TIMEOUT_NOT_CONTACT.value])
        ).first()

        if win_partic:
            # 清除缓存partic信息
            Partic.clear_demand_partic_cache_data(current_app.redis, demand.id)
            return Response(BID_ALREADY)
        if not self.user.pack:
            return Response(NO_PACK)

        # 剩余汇币或者可用的最大代金券额
        max_bid_point = max(
            Voucher.max_voucher_bid_point(self.user.id),
            self.user.pack.bid_point
        )

        if max_bid_point < offer:
            return Response(OUT_OF_PACK)

        # 爆灯不判断是否高于最高出价
        if offer < MAX_OFFER:
            # 进行中的项目报价不可低于起拍点
            if (demand.status == DemandStatus.ONGOING.value and
                    offer < demand.bid_start_point):
                return Response(BID_LOWER_THAN_START)

            # 若提供的报价低于项目已有的最高报价, 则报价失败(仅当项目在首次竞价中时判断)
            if (demand.status == DemandStatus.ONGOING.value and
                    offer <= demand.max_partic_offer):
                # 清除缓存partic信息
                Partic.clear_demand_partic_cache_data(current_app.redis, demand.id)
                return Response(BID_LOWER)
        else:
            # 爆灯修改出价为爆灯价
            offer = MAX_OFFER

        partic = Partic.get_by_args(
            demand_id=demand.id,
            supplier_id=self.user.id,
            published_count=demand.published_count,
        )

        # 项目为一口价或者免费竞价
        # 同时工程商已经在当前项目状态下抢过单了
        if partic and partic.demand_status == demand.status \
                and demand.status in (
                DemandStatus.SECOND_ONGOING.value,
                DemandStatus.FREE_ONGOING.value):
            return Response(BID_ALREADY)

        # 出价需高于自身出价(仅首次竞价的项目需要判断)
        if (
            demand.status == DemandStatus.ONGOING.value
            and offer < MAX_OFFER
            and partic and offer <= partic.offer
        ):
            return Response(BID_LOWER_THAN_SELF)

        if partic:
            partic.status = ParticStatus.WAITING.value
            partic.offer = offer
            partic.offer_at = int(time.time())
            partic.demand_status = demand.status
            partic.is_training = False
            partic.is_revert = False
        else:
            # 写入抢单表, 并记录竞价点
            partic = Partic.create(
                demand_id=demand.id,
                supplier_id=self.user.id,
                status=ParticStatus.WAITING.value,
                offer=offer,
                offer_at=int(time.time()),
                demand_status=demand.status,
                credit=self.user.credit,
                published_count=demand.published_count
            )
            demand.bid_cnt += 1
            # 只有首次出价的进行记录
            current_app.zone_stat_exchanger.exchange_send(json.dumps({
                'bid_cnt': 1,
                'bid_user_id': self.user.id,
                'war_zone_id': self.user.war_zone_id
            }))
            if demand.status == DemandStatus.ONGOING.value:
                self.user.credit += UserCreditRule.ONGOING_OFFER.value

        try:
            db.session.commit()

            # TO1, TO2,政府订单, 无中标流程
            if demand.tag not in (
                DemandTag.VIRT_ONE.value,
                DemandTag.VIRT_TWO.value,
                DemandTag.GOV_RO.value
            ):
                if demand.status in (
                    DemandStatus.FREE_ONGOING.value,
                    DemandStatus.SECOND_ONGOING.value
                ):
                    current_app.winner_producer.send(
                        json.dumps(dict(
                            func='assign_winner',
                            args=(),
                            kwargs=dict(
                                partic_ids=[partic.id],
                                demand_id=demand.id,
                                published_count=demand.published_count
                            )
                        ))
                    )

                if demand.status == DemandStatus.BRONZE_MEDAL_DISCOUNT.value:
                    current_app.winner_producer.send(
                        json.dumps(dict(
                            func='bronze_assign_winner',
                            args=(),
                            kwargs=dict(
                                partic_id=partic.id,
                                demand_id=demand.id,
                                published_count=demand.published_count
                            )
                        ))
                    )
        except:
            import traceback
            print(traceback.format_exc())
            db.session.rollback()

        # 清除缓存partic信息
        Partic.clear_demand_partic_cache_data(current_app.redis, demand.id)

        return Response(SUCCESS, {
            'demand_info': generate_demand_info_json(demand, self.user)
        })


@api.resource('/supplier/feedback/types')
class FeedbackTypesResource(AuthResource):
    """
    created by chenhj at 2018/4/2

    """
    def post(self):
        """
        created by chenhj at 2018/4/2

        """
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        data, errors = demand_id_in_schema.load(request.form)
        if errors:
            return Response(PARAMS_ILLEGAL.new_format_msg(list(errors.values())[0][0]))

        user_info = UserInfo.default_user_info(self.user.id)
        partic = Partic.get_by_args(
            demand_id=data['demand_id'],
            supplier_id=self.user.id,
        )
        # add by zhouhl 2018-07-28 后台日志发现部分没有抢过单的用户访问接口
        if not partic:
            return Response(NO_PARTIC)

        enable = True
        info = ''
        cannot_touch_info = '多次拨打不影响发起退款，销售机会来之不易，请尝试不同时段多次联系客户'
        touch_enable = True
        # 与用户通话60s以上, 不允许反馈联系不到用户
        if partic.contact_seconds > 60:
            cannot_touch_info = '您已与客户通话 {:0>2}:{:0>2}, 不能提交该选项'.format(
                partic.contact_seconds // 60, partic.contact_seconds % 60
            )
            touch_enable = False
        else:
            virtual_number_data = VirtualNumber.get_by_args(
                demand_id=data['demand_id'],
                tel_b=self.user.mobile
            )
            if virtual_number_data:
                call_data = QueryCallNumberRecord.query.filter(
                    QueryCallNumberRecord.created_at >= partic.bid_win_at,
                    QueryCallNumberRecord.caller == virtual_number_data.tel_b,
                    QueryCallNumberRecord.called == virtual_number_data.tel_x
                ).order_by(
                    QueryCallNumberRecord.created_at.desc()
                )
                # 呼叫次数未达到3次, 不允许反馈呼叫不到客户
                if call_data.count() < 3:
                    cannot_touch_info = '您呼叫客户次数未满3次, 不能提交该选项'
                    touch_enable = False
                # 第一个呼叫与最后一次呼叫之间间隔未达到15分钟, 不允许反馈呼叫不到客户
                elif call_data[0].created_at - call_data[-1].created_at <= 15 * 60:
                    cannot_touch_info = '呼叫客户时间间隔不足15分钟，不能提交该选项'
                    touch_enable = False
        reason_left_times = self.user.reason_left_times
        free_reason_feedback_cnt = user_info.free_reason_feedback_cnt
        # if free_reason_feedback_cnt <= 0 and reason_left_times <= 0:
        #     enable = False
        #     info = '您的退款机会已全部用完'

        # 超过1天不允许发起反馈
        if get_timestamp() - partic.bid_win_at >= 60 * 60 * 24:
            enable = False
            info = '超过24小时, 不支持退款'
        # 拨打前获取真实号码不允许发起反馈
        if partic.apply_real_number_state == ApplyRealNumberState.GET_BEFORE.value:
            enable = False
        # FIXME by chenhj at 2018/4/25 派单不允许退单
        if partic.demand_status in (
            DemandStatus.DISPATCHING.value, DemandStatus.MASTER_DISPATCHING.value
        ):
            enable = False
        # 代金券支付不允许发起反馈
        if partic.voucher_id != 0:
            enable = False
        return Response(
            SUCCESS,
            {
                'body': {
                    'locked_paid_bid_point': LOCKED_PAID_BID_POINT,
                    'enable': enable,
                    'info': info,
                    'free_reason_left_times': free_reason_feedback_cnt,
                    'free_reason_value': Feedback.NO_REASON.value,
                    'free_reason_info': '' if free_reason_feedback_cnt > 0 else '剩余无理由退款0次, 无法使用',
                    'reason_left_times': reason_left_times,
                    'reason_info': '' if reason_left_times > 0 else '本月退款已被驳回{}次, 无法使用'.format(MONTHLY_REFUSE_MAX),
                    'feedback_contact_user': {
                        'nickname': '周经理',
                        'contact': 'WPHHZ99',
                        'type': '微信号'
                    },
                    'reason_options': [
                        {
                            'option': Feedback.NO_DEMAND.display,
                            'value': Feedback.NO_DEMAND.value,
                            'enable': True,
                            'info': '及时联系、积极营销、阐述优势，方能打动客户。'
                        }, {
                            'option': Feedback.CANNOT_TOUCH.display,
                            'value': Feedback.CANNOT_TOUCH.value,
                            'enable': touch_enable,
                            'info': cannot_touch_info
                        }, {
                            'option': Feedback.DESC_WRONG.display,
                            'value': Feedback.DESC_WRONG.value,
                            'enable': True,
                            'info': '个人原因看错信息抢错单不予退款。小小的误会也许并不影响你们的缘分。'
                        }, {
                            'option': Feedback.OTHER.display,
                            'value': Feedback.OTHER.value,
                            'enable': True,
                            'info': ''
                        }
                    ]
                }
            }
        )


@api.resource('/supplier/feedback')
class FeedbackResource(AuthResource):
    """
    信息反馈
    """
    def post(self):
        """
        create by ld_wu 2017-09-12
        """
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        data, errors = feedback_schema.load(request.form)
        if errors:
            return Response(
                PARAMS_ILLEGAL.new_format_msg(list(errors.values())[0][0])
            )

        partic = Partic.get_by_args(
            demand_id=data['demand_id'],
            supplier_id=self.user.id,
            status=ParticStatus.SUCCEED.value
        )
        if not partic:
            return Response(NO_PARTIC)

        now = get_timestamp()
        feedback_type = data.get('feedback_type')
        feedback = data.get('feedback')

        if partic.feedback:
            return Response(FEEDBACK_DUPLICATE)

        if partic.follow_type == FollowType.LOCKED.value:
            return Response(LOCKED_CANNOT_FEEDBACK)

        if feedback_type:
            if partic.follow_type != FollowType.KEEP.value:
                return Response(MUST_BE_KEEP)

            if self.user.pack.bid_point < LOCKED_PAID_BID_POINT:
                return Response(LOW_BID_POINT)

            demand = Demand.get_by_id(partic.demand_id)
            # 判断订单是否已被锁定
            if demand.is_locked:
                return Response(DEMAND_LOCKED)

            if demand.status == DemandStatus.DONE.value:
                return Response(DEMAND_DONE)

            # 扣除锁定汇币
            self.user.pack.bid_point -= LOCKED_PAID_BID_POINT

            # 日统计
            current_app.zone_stat_exchanger.exchange_send(json.dumps({
                'locked_cnt': 1,
                'cost_bid_point': LOCKED_PAID_BID_POINT,
                'cost_bid_user_id': self.user.id,
                'war_zone_id': self.user.war_zone_id
            }))
            current_app.common_producer.send(
                json.dumps(
                    dict(
                        backend='bid_stock_stat',
                        func='day_stat',
                        kwargs=dict(
                            stat_type='cost_bid_point',
                            stat_cnt=LOCKED_PAID_BID_POINT,
                        )
                    )
                )
            )

            BidCostRecord.create(
                user_id=self.user.id,
                cost_type=BidCostType.LOCKED.value,
                bid_point=LOCKED_PAID_BID_POINT,
                left_bid_point=self.user.pack.bid_point,
                demand_id=partic.demand_id
            )
            # 修改跟进状态
            partic.follow_type = FollowType.LOCKED.value
            partic.feedback_at = now
            partic.feedback_status = FeedbackStatus.PASS.value

            demand.status = DemandStatus.COMPETE_DONE.value

            # 判断是否需要退返派单点
            current_app.common_producer.send(
                json.dumps(
                    dict(
                        backend='demand_backend',
                        func='lock_handle',
                        kwargs=dict(
                            demand_id=demand.id,
                        )
                    )
                )
            )
        else:
            # 派单不允许反馈
            if partic.demand_status in (
                DemandStatus.DISPATCHING.value, DemandStatus.MASTER_DISPATCHING.value
            ):
                return Response(DISPATCH_CANNOT_FOLLOW)

            # 超过1天不允许发起反馈
            if now - partic.bid_win_at >= 60 * 60 * 24:
                return Response(ONE_DAY_DISABLE_FEEDBACK)
            if partic.apply_real_number_state == ApplyRealNumberState.GET_BEFORE.value:
                return Response(APPLY_REAL_NUM_ALREADY)
            if partic.voucher_id != 0:
                return Response(VOUCHER_PARTIC_CANNOT_FEEDBACK)

            # 无理由
            if feedback == Feedback.NO_REASON.value:
                user_info = UserInfo.default_user_info(self.user.id)
                # 判断用户是否仍有无理由发起次数
                if user_info.free_reason_feedback_cnt <= 0:
                    return Response(OUT_OF_FREE_REASON_CNT)
                # 直接通过, 并减一次无理由次数
                user_info.free_reason_feedback_cnt -= 1
                partic.feedback_at = now
                partic.feedback = feedback
                partic.feedback_content = data.get('feedback_content', '')
                partic.feedback_pass()

            else:
                # 有理由
                if self.user.reason_left_times <= 0:
                    return Response(OUT_OF_REASON_CNT)

                partic.feedback_at = now
                partic.feedback = feedback
                partic.feedback_content = data.get('feedback_content', '')
                partic.feedback_status = (
                    partic.feedback_status or FeedbackStatus.PENDING.value
                )

        return Response(
            SUCCESS,
            dict(
                my_offer=int(partic.offer),
                # punish_bid_point=punish_bid_point,
                feedback_status=partic.feedback_status
            )
        )


@api.resource('/Supplier/Supplier/certify')
class SupCertifyResource(AuthResource):
    """
    created by chenhj at 2017/8/14
    工程商认证
    """
    def post(self):
        """
        created by chenhj at 2017/8/14

        """
        if self.user.status == UserStatus.PASS.value:
            return Response(SUP_ALREADY_AUTH)

        data, errors = user_edit_schema.load(request.form)
        if errors:
            return Response(PARAMS_ILLEGAL.new_format_msg(list(errors.values())[0][0]))

        self.user.type = UserType.ENGINEER.value
        self.user.level = UserLevel.LEVEL_2.value
        self.user.status = UserStatus.PENDING.value
        self.user.active = UserActive.ACTIVE.value

        # 更新用户信息
        self.user.nickname = data['nickname']
        self.user.gender = data['gender']
        self.user.prov = data['prov']
        self.user.city = data['city']
        if 'com' in data and data['com']:
            self.user.com = data['com']
        area_id = Area.get_area_id(data['prov'], data['city'])
        if area_id:
            self.user.area_id = area_id

        # 写入默认标签
        UserTag.default_tag(self.user.id)

        # 签署用户协议
        protocol_version = current_app.redis.get(PROTOCOL_VERSION_KEY) or ''
        if protocol_version and not UserProtocol.get_by_args(
                user_id=self.user.id, version=protocol_version
        ):
            UserProtocol.create(
                user_id=self.user.id,
                mobile=self.user.mobile,
                nickname=self.user.nickname,
                version=protocol_version
            )

        # 拉新用户如果未分级需进行分级, 或者停止新单短信
        self.user.be_engineer_sms_handler()

        # 若没有套餐, 表示是初次绑定, 赠送套餐
        if not self.user.pack:
            self.user.gift_bid_point()

        return Response(SUCCESS)


@api.resource('/Supplier/Pack/getMyPack')
class MyPackResource(AuthResource):
    """
    created by chenhj at 2017/8/11
    我的套餐信息
    """
    def post(self):
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        tag = UserTag.get_by_args(user_id=self.user.id)
        return Response(SUCCESS, {
            'pack': pack_schema.dump(self.user.pack)[0],
            'user_tags': user_tag_schema.dump(tag)[0],
            'business_keys': BusinessDisplay,
            'available_vouchers': vouchers_schema.dump(
                Voucher.available_vouchers(self.user.id)
            )[0],
            'unavailable_vouchers': vouchers_schema.dump(
                Voucher.unavailable_vouchers(self.user.id)
            )[0],
        })


@api.resource('/Supplier/Pack/updateMyPackProv')
class UpdatePackProvResource(AuthResource):
    """
    created by chenhj at 2017/8/14
    """

    def post(self):
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)
        if not self.user.pack:
            return Response(NO_PACK)
        # 地方合作伙伴不允许修改关注区域
        if self.user.level == UserLevel.LEVEL_6.value:
            return Response(IS_USER_LEVEL_6)

        provs = request.form.getlist('prov[]')

        # 只查省份
        areas = Area.query.filter(
            Area.name.in_(provs), Area.pid == 0
        )
        area_ids = []
        for area in areas:
            area_ids.extend([x.id for x in area.cities])
        # self.user.pack.area_id = area_ids or [self.user.area_id]
        self.user.attention_areas = Area.get_list_by_ids(area_ids).all() \
            or Area.get_list_by_ids(self.user.area_id).all()

        return Response(SUCCESS, {
            'pack': pack_schema.dump(self.user.pack)[0]
        })


@api.resource('/supplier/pack/area')
class UpdatePackAreaResource(AuthResource):
    """
    create by ld_wu 2017-09-01
    更新工程商地域范围
    由于是一个省一个省来进行更新，所以需要移除该省原有的再加入新的
    """

    def post(self):
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)
        if not self.user.pack:
            return Response(NO_PACK)
        # 地方合作伙伴不允许修改关注区域
        if self.user.level == UserLevel.LEVEL_6.value:
            return Response(IS_USER_LEVEL_6)

        areas = [int(each) for each in request.form.getlist('areas[]')]

        # current_areas = set(self.user.pack.area_id or list())
        current_areas = set([i.id for i in self.user.attention_areas] or list())

        # 是否为全国，True为全国
        op_type = request.form.get('type', type=int)
        if op_type == OpType.ALL:
            areas = Area.get_list_by_args(type=AreaType.CITY.value)
            # 需要移除台湾，台湾用于测试
            new_areas = [each.id for each in areas if each.id != TAIWAN_CODE]
        elif op_type == OpType.UPDATE:
            # 移除该省份下的所有城市id
            # 加入新的城市id
            subquery = Area.query.filter(
                Area.id.in_(areas)
            ).with_entities(
                Area.pid
            ).subquery()

            remove_city = Area.query.filter(
                Area.pid.in_(subquery),
                Area.type == AreaType.CITY.value
            )
            # 需要移除的原省份城市集合
            remove_set = set([each.id for each in remove_city])

            new_areas = current_areas - remove_set | set(areas)
        elif op_type == OpType.REMOVE:
            # 移除某省的关注
            remove_city = Area.query.filter(
                Area.pid.in_(areas),
                Area.type == AreaType.CITY.value
            ).with_entities(
                Area.id
            )
            remove_set = set([each.id for each in remove_city])
            new_areas = current_areas - remove_set
        elif op_type == OpType.MUNICIPAL:
            # 直辖市处理逻辑
            new_areas = (
                (current_areas - MUNICIPAL_CODE) | (set(areas) & MUNICIPAL_CODE)
            )
        else:
            new_areas = current_areas

        if not new_areas and self.user.prov:
            new_areas = Area.get_city_ids(self.user.prov)

        self.user.attention_areas = Area.get_list_by_ids(
            sorted([int(each) for each in new_areas])
        ).all()
        self.user.has_set_business = True

        return Response(SUCCESS, {
            'pack': pack_schema.dump(self.user.pack)[0]
        })


@api.resource('/supplier/tags')
class UpdateTagsResource(AuthResource):
    """
    create by ld_wu 2017-09-01
    更新工程商业务范围
    """

    def post(self):
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        print('supplier change tags:{user_id}, {tag}'.format(
            user_id=self.user.id, tag=request.form
        ))
        tag = UserTag.get_by_args(user_id=self.user.id) or UserTag()
        tag.user_id = self.user.id
        for key in BusinessDisplay.keys():
            setattr(tag, key, request.form.get(key, False))
        self.user.has_set_business = True
        db.session.add(tag)
        return Response(
            SUCCESS,
            dict(
                user_tags=user_tag_schema.dump(tag)[0],
                business_keys=BusinessDisplay
            )
        )


@api.resource('/Supplier/Supplier/join')
class SupplierJoinResource(Resource):
    """
    created by chenhj at 2017/8/21
    工程商加盟
    """
    def post(self):
        data, errors = supplier_join_schema.load(request.form)
        if errors:
            return Response(PARAMS_ILLEGAL.new_format_msg(list(errors.values())[0][0]))

        mobile = data['mobile']
        user = User.get_by_args(mobile=mobile)
        area_id = Area.get_area_id(data['prov'], data['city']) or -1
        # 若用户未注册, 则创建用户且标记为工程商
        if not user:
            user = User.create(
                area_id=area_id,
                type=UserType.ENGINEER.value,
                mobile=mobile,
                nickname=data['nickname'],
                gender=data['gender'],
                prov=data['prov'],
                city=data['city'],
                addr=data.get('addr') or '',
                com=data.get('com') or '',
                pos=data.get('pos') or '',
                status=UserStatus.PENDING.value
            )
            resp = jsonify(Response(SUCCESS))
            resp.set_cookie('opc_token', AuthToken.generate_auth_token(TOKEN_TIMEOUT, user))
            return resp

        # 若未登录, 则发送验证码, 则返回未登录
        token = request.cookies.get('opc_token')
        login_user = AuthToken.verify_auth_token(token)
        if not login_user:
            captcha_sms_channel = current_app.redis.get(Captcha_Sms_Channel_Key) or CaptchaSmsChannel.MENG_WANG.value
            key = 'captcha_{}'.format(mobile)
            captcha = current_app.redis.get(key)
            if not captcha:
                if captcha_sms_channel == CaptchaSmsChannel.NA.value:
                    captcha = '1234'
                else:
                    captcha = generate_captcha()
                current_app.redis.set(key, captcha, ex=30 * 60)

            template_code = 'NA'
            if captcha_sms_channel == CaptchaSmsChannel.MENG_WANG.value:
                template_code = 'CAPTCHA'
                content = generate_sms_content(
                    template_code=template_code,
                    captcha=captcha
                )
                current_app.captcha_producer.send(
                    json.dumps({
                        'mobile': mobile,
                        'content': content,
                    })
                )
            elif captcha_sms_channel == CaptchaSmsChannel.ALI_DA_YU.value:
                # 使用阿里大于发送短信
                template_code = 'SMS_52355022'
                req = alidayu.api.AlibabaAliqinFcSmsNumSendRequest()
                req.set_app_info(
                    alidayu.appinfo('23382326', 'd6932553edc60a55fdd84e6efb1bd262'))
                req.sms_type = 'normal'
                req.rec_num = mobile
                req.sms_template_code = template_code
                req.sms_free_sign_name = 'LED屏'
                req.sms_param = {'captcha': captcha}
                req.getResponse()
            SMSLog.create(
                mobile=mobile,
                template_code=template_code,
                content=captcha,
            )

            return Response(UNAUTHORIZED)

        # 若已经是工程商, 则返回提示
        if user.type == UserType.ENGINEER.value:
            return Response(ALREADY_ENGINEER)

        # 普通用户, 则更新信息, 并标记为工程商
        if user.mobile == login_user.mobile:
            user.type = UserType.ENGINEER.value
            user.level = UserLevel.LEVEL_2.value
            user.nickname = data['nickname']
            user.gender = data['gender']
            user.prov = data['prov']
            user.city = data['city']
            user.area_id = area_id
            user.addr = data.get('addr') or ''
            user.com = data.get('com') or ''
            user.pos = data.get('pos') or ''
            user.status = UserStatus.PENDING.value
            # 写入默认标签
            UserTag.default_tag(user.id)

            if user.wx_openid:
                wxuser = WxUser.get_by_args(openid=user.wx_openid)
                if wxuser and wxuser.invite_user_id and wxuser.invite_state == WxInviteState.NOT_BIND.value:
                    wxuser.invite_state = WxInviteState.BINDED.value
                    invite_user = User.get_by_id(wxuser.invite_user_id)
                    if invite_user:
                        invite_user.credit += UserCreditRule.INVITE.value

            # 若没有套餐, 表示是初次绑定, 赠送套餐
            if not user.pack:
                user.gift_bid_point()

        return Response(SUCCESS)


@api.resource('/Supplier/Demand/particAsChat')
class ParticChatResource(AuthResource):
    """
    created by chenhj at 2017/8/22

    """
    def post(self):
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        data, errors = demand_id_in_schema.load(request.form)
        if errors:
            return Response(PARAMS_ILLEGAL.new_format_msg(list(errors.values())[0][0]))
        demand = Demand.get_by_id(data['demand_id'])
        if not demand:
            return Response(DEMAND_NOT_EXIST)

        partic = Partic.get_by_args(
            demand_id=demand.id,
            supplier_id=self.user.id,
            published_count=demand.published_count
        )
        if partic:
            return Response(PARTIC_ALREADY)

        Partic.create(
            demand_id=demand.id,
            supplier_id=self.user.id,
            status=ParticStatus.TALKED.value,
            published_count=demand.published_count
        )

        return Response(SUCCESS, {
            'info': generate_demand_info_json(demand, self.user)
        })


@api.resource('/Supplier/Supplier/activate')
class SupplierActivateResource(AuthResource):
    """
    created by chenhj at 2017/8/23

    """
    def post(self):
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        if not self.user.avatar:
            return Response(ACTIVATE_NO_AVATAR)

        data, errors = supplier_activate_schema.load(request.form)
        if errors:
            return Response(PARAMS_ILLEGAL.new_format_msg(list(errors.values())[0][0]))

        self.user.nickname = data['nickname']
        self.user.active = UserActive.ACTIVE.value
        return Response(SUCCESS)


@api.resource('/Supplier/invite/getList')
class SupplierInviteListResource(AuthResource):
    """
    created by chenhj at 2017/9/7
    邀请成功列表
    """
    def post(self):
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        invite_user = WxUser.get_list_by_args(
            invite_user_id=self.user.id,
            invite_state=WxInviteState.BINDED.value
        )
        not_bind_user = WxUser.get_list_by_args(
            invite_user_id=self.user.id,
            invite_state=WxInviteState.NOT_BIND.value
        )
        user_openids = [x.openid for x in invite_user]
        if not user_openids:
            cnt = 0
            data = []
        else:
            sql = text("""
                select wxuser.headimgurl, user.nickname, mobile, invite_time, invite_buy_pack,invite_pack_price
                 from user join wxuser
                on user.wx_openid = wxuser.openid
                where openid in :openids
            """)
            users = db.session.execute(sql, {'openids': user_openids})
            data = supplier_invite_users_schema.dump(users)[0]
            cnt = len(data)
        not_bind_data = supplier_invite_users_schema.dump(not_bind_user)[0]
        return Response(SUCCESS, {
            'cnt': cnt,
            'data': data,
            'not_bind_data': not_bind_data
        })


@api.resource('/Supplier/invite/create')
class SupplierInviteCreateResource(AuthResource):
    """
    created by chenhj at 2017/9/7
    生成邀请卡
    """
    def post(self):
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        openid = self.user.wx_openid
        media_id = current_app.redis.get(INVITE_CARD_CACHE_KEY.format(self.user.id))
        if not media_id:
            resp = client.qrcode.create({
                'action_name': 'QR_LIMIT_STR_SCENE',
                'action_info': {
                    'scene': {'scene_str': openid}
                }
            })
            url = resp['url']
            filename = generate_invite_card(self.user, url)
            f = open(filename, 'rb')
            resp = client.media.upload(media_type='image', media_file=f)
            f.close()
            if os.path.exists(filename):
                os.remove(filename)
            media_id = resp['media_id']
            current_app.redis.set(
                INVITE_CARD_CACHE_KEY.format(self.user.id), media_id, INVITE_CARD_CACHE_EX
            )
        if isinstance(media_id, bytes):
            media_id = media_id.decode('utf8')
        try:
            client.message.send_image(openid, media_id)
        except WeChatClientException as e:
            print('*' * 80)
            print(e.errcode)
            template = WechatTemplate.get_by_args(code='invite_card_service')
            data = template.content.replace(
                'keyword2_value', datetime.now().strftime('%F %T')
            )
            to_send_data = [{
                'openid': openid,
                'template_id': template.template_code,
                'data': json.loads(data),
                'url': ''
            }]
            current_app.wechat_producer.send(json.dumps(to_send_data))
            return Response(SUCCESS)
        client.message.send_text(openid, '长按图片即可转发给好友')

        return Response(SUCCESS)


@api.resource('/supplier/demand/apply_real_number')
class SupplierApplyRealNumberResource(AuthResource):
    """
    created by chenhj at 2018/6/27
    申请获取用户真实号码接口
    """
    def post(self):
        """
        created by chenhj at 2018/6/27

        """
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        data, errors = demand_id_in_schema.load(request.form)
        if errors:
            return Response(PARAMS_ILLEGAL.new_format_msg(list(errors.values())[0][0]))
        demand = Demand.get_by_id(data['demand_id'])
        if not demand:
            return Response(DEMAND_NOT_EXIST)

        partic = Partic.get_by_args(
            demand_id=demand.id,
            supplier_id=self.user.id,
            status=ParticStatus.SUCCEED.value
        )
        if not partic:
            return Response(NO_PARTIC)

        # 放弃跟进后不允许申请真实号码
        if partic.follow_type == FollowType.GIVE_UP.value:
            return Response(GIVE_UP_CANNOT_APPLY_REAL_NUM)

        if partic.apply_real_number_state != ApplyRealNumberState.NA.value:
            return Response(APPLY_REAL_NUM_ALREADY)

        # 不允许拨打前获取
        if partic.contact_state != ContactState.YES.value:
            return Response(CAN_NOT_GET_BEFORE)

        partic.apply_real_number_state = ApplyRealNumberState.GET_AFTER.value

        # 记录申请真实号码时间
        partic.apply_real_number_at = get_timestamp()

        demand_client = User.get_by_id(demand.uid)

        db.session.commit()
        Partic.clear_demand_partic_cache_data(current_app.redis, demand.id)

        return Response(SUCCESS, {
            'real_number': demand_client.mobile,
            'apply_real_number_state': partic.apply_real_number_state
        })


@api.resource('/supplier/redeem_code')
class SupplierRedeemCodeResource(AuthResource):
    """
    created by chenhj at 2018/9/25
    工程商使用兑换码
    """
    def post(self):
        """
        created by chenhj at 2018/9/25

        """
        # 不是工程商则直接返回
        if self.user.type != UserType.ENGINEER.value:
            return Response(NOT_ENGINEER)

        data, errors = redeem_code_schema.load(request.form)
        if errors:
            return Response(PARAMS_ILLEGAL.new_format_msg(list(errors.values())[0][0]))

        redeem_code = RedeemCode.get_by_args(code=data['code'])
        if not redeem_code:
            return Response(REDEEM_NOT_EXISTED)

        if redeem_code.status == RedeemStatus.INACTIVE.value:
            return Response(REDEEM_INACTIVE)

        if redeem_code.redeem_expired_date < date.today():
            redeem_code.status = RedeemStatus.INACTIVE.value
            return Response(REDEEM_INACTIVE)

        if redeem_code.status == RedeemStatus.USED.value:
            return Response(REDEEM_USED)

        if redeem_code.gift_type == RedeemGiftType.VOUCHER.value:
            gift_cnt = redeem_code.count
            code = redeem_code.code
            gift_days = redeem_code.gift_days
            gift_amount = redeem_code.gift_amount

            expired_date = date.today() + timedelta(days=gift_days)
            for _ in range(gift_cnt):
                Voucher.create(
                    user_id=self.user.id,
                    bid_point=gift_amount,
                    reason=GiveVoucherReason.MARKETING.value,
                    redeem_code=code,
                    expired_date=expired_date
                )

        redeem_code.status = RedeemStatus.USED.value
        redeem_code.user_id = self.user.id
        redeem_code.redeem_date = date.today()
        return Response()
